## -*- coding: UTF-8 -*-

"""
Module representing "browser" tab
"""

import pygtk
import gtk
import gobject
import re
import time
import Image
import os
import sys
import inspect
import shutil
import gtkmozembed
import gettext
from cgi import escape

from . import editmetadata
from .. import path
from .. import html
from .. import log
from .. import database
from .. import settings
from .. import exceptions
from .. import core
from .. import collection
from .. import bookmarks
from .. import genres
from .. import xml
#from .. import mozembed
from .. import strunicode as su
from ..timing import Timing



url_handlers = dict()
context_handlers = dict()

def initialize():
    """
    Initialize browser-parts
    """
    global url_handlers
    
    from .browserparts import collection as bp_collection
    from .browserparts import shelves as bp_shelves
    
    bp_collection.initialize()
    bp_shelves.initialize()
    
    url_handlers.update(bp_collection.renderers)
    url_handlers.update(bp_shelves.renderers)

    context_handlers.update(bp_collection.context_handlers)
    context_handlers.update(bp_shelves.context_handlers)
    
def parse_query_string(query):
    """
    query consists of "words", each word is string 
    @return: list of pairs (keyword, pattern).
    """
    orig_query = query
    query = query.strip() + " "
    result = list()
    current_pattern = ""
    
    quoted = False
    is_keyword = True
    
    prev_sym = ""
    
    keyword_re = re.compile(u'^([+]?)([a-zа-я]+):([\w]+)\s', flags=re.UNICODE)
    quoted_keyword_re = re.compile(u'^([+]?)([a-zа-я]+):"([^"\W]+)"\s', flags=re.UNICODE)
    
    for sym in query:
        current_pattern += sym
        keyword = None
        sign = ""
        
        mo = keyword_re.search(current_pattern)
        if mo is not None:
            keyword = mo.group(2)
            sign = mo.group(1)
            pattern = mo.group(3)
            
        mo = quoted_keyword_re.search(current_pattern)
        if mo is not None:
            keyword = mo.group(2)
            sign = mo.group(1)
            pattern = mo.group(3)
            
        if keyword is not None:
            result.append((keyword, pattern, sign))
            current_pattern = ""
            
    if keyword is None:
        # i.e. string is not completely parsed
        result = [ ("", orig_query, "") ]
            
    return result
class BrowserHistory(gobject.GObject):
    """
    Class that maintains history of "visited pages" and navigation between them.
    """
    __gsignals__ = {
        'history-pos-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (int, ))
        }
    
    def __init__(self, moz_widget, hist_back_button, hist_forward_button):
        """
        Constructor.
        
        @param moz_widget: gtkmozembed.MozEmbed instance that should be used as browser.
        @param hist_back_button: gtk.Widget that represents history navigation button "Back"
        @param hist_forward_button: gtk.Widget that represents history navigation button "Forward"
        """
        gobject.GObject.__init__(self)
        
        self.moz_widget = moz_widget
        self.moz_location = None
        self.hist_forward_button = hist_forward_button
        self.hist_back_button = hist_back_button
        
        self.stack = []
        self.stack_pos = -1
        self.index = 0
        
        self.moz_widget.connect("net-stop", self.on_document_loaded)
        self.moz_widget.connect("location", self.on_location)
       
        # init cache dir for the pages
        cache_root = path.cache_dir
        # create unique directory name
        self.cache_dir = None
        for n in range(1000): # 1000 attempts
            dir = os.path.join(cache_root, "browser_%d" % n)
            if not os.path.exists(dir):
                try:
                    os.mkdir(dir)
                    self.cache_dir = dir
                    break
                except OSError:
                    log.error("It's impossible to create directory for temporary files")
                    raise exceptions.BrowserError("Unable to create cache dir `%s'" % dir)
                
        if self.cache_dir is None:
            raise exceptions.BrowserError("Unable to create any cache directory for some reasons")
            
    def __del__(self):
        log.debug("BrowserHistory.__del__() called")
        
    def on_document_loaded(self, widget):
        """
        Update state of history navigation button: enable or disable them according to 
            current position in history.
            
        @param widget: 
        """
        #log.debug("History: items(%s), pos(%s)" % (len(self.stack), self.stack_pos))
        b_state = False
        f_state = False
        if self.moz_widget.can_go_back():
            b_state = True
            
        if self.moz_widget.can_go_forward():
            f_state = True
        
        self.hist_back_button.set_sensitive(b_state)
        self.hist_forward_button.set_sensitive(f_state)
        
    def on_location(self, widget):
        log.debug("Location changed")
        self.moz_location = widget.get_location()
        self.on_document_loaded(widget)
        
    def replace_to(self, url, content, page_files=None, title=""):
        """
        Replace currently displayed page with new, not moving in history.
        
        @param url: new page url
        @param content: page contents
        @param page_files: list of files that should be deleted after 
            removing page from the history
        @param title: page title
        """
        if page_files is None:
            page_files = []
            
        if self.stack_pos >= 0:
            old_page_files = self.stack[self.stack_pos]['page_files']
            file = old_page_files[0] # first item is always loaded html file name
            page_files.insert(0, file)
            
            data = html.gen_html_page(body=content, title=title)
            try:
                f = open(file, "w")
                f.write(data)
                f.close()
                self.moz_widget.reload(gtkmozembed.FLAG_RELOADNORMAL)
                del self.stack[self.stack_pos]['page_files']
                self.stack[self.stack_pos]['page_files'] = page_files
                #self.moz_widget.render_data(data, os.path.getsize(file), "file://" + file, "text/html")
            except IOError, e:
                log.error("replace_to() IOError: %s" % e)
        
    def go_to(self, url, content=None, path=None, page_files=None, title=""):
        """
        Display new page in the browser and remove pages from history after current one
        
        @param url: url of page to be displayed
        @param content: content of the page
        @param path: path to file with content
        @param page_files: list of files that should be deleted after 
            removing page from the history
        @param title: page title
        """
        if page_files is None:
            page_files = []
        # page_files is a list of files linked to displayed page, e.g. images.
        # these files must be erased along with html page from the cache
        log.debug("History: append page `%s'" % (url))
        
        # write cache file
        self.index += 1
        
        data = None
        if content is None:
            # if content is None, path should exist
            assert path
            assert os.path.exists(path)
        else:
            file = os.path.join(self.cache_dir, "%s.html" % self.index)
            log.debug("Cached page file is: %s" % file)
            data = html.gen_html_page(body=content, title=title)

        try:
            # remove all other pages from cache, i.e. all pages with position more than current position
            stack_top = len(self.stack)-1

            while stack_top > self.stack_pos:
                # remove all linked pages 
                for f in self.stack[stack_top]['page_files']:
                    try:
                        os.remove(f)
                    except OSError, e:
                        log.error("OSError: %s" % e)
                
                # remove top element of the stack itself
                del self.stack[stack_top]
                stack_top -= 1

            if not content is None:
                f = open(file, "w")
                f.write(data)
                f.close()
                gobject.idle_add(self.moz_widget.load_url, "file://" + file)
                page_files.insert(0, file)
            else:
                gobject.idle_add(self.moz_widget.load_url, "file://" + path)
            
            
            books_on_page = []
            if data is not None:
                books_on_page = re.findall("book://([0-9]+)", data)
            
            self.stack.append( {'url':url, 'page_files': page_files, 'page_title': title, 'books_on_page': books_on_page} )
            self.stack_pos += 1
            
            self.emit("history-pos-changed", self.stack_pos)
        except IOError, e:
            log.error("IOError: %s" % e)
    
    def __go(self, direction, check_func, move_func):
        """
        Go to next or previous page from the history.
        @param direction: 1 for moving forward, -1 for moving back
        @param check_func: callback function used for checking is 
            requested move possible
        @param move_func: callback function performing actual movement
        """
        if direction > 0:
            direction = 1
        else:
            direction = -1
            
        if check_func() is not None:
            def location_change(_old_loc, _direction):
                new_loc = self.moz_location
                if new_loc == _old_loc:
                    return True
                
                log.debug("Old loc `%s', new loc `%s'" % (_old_loc, new_loc))
                old_base = _old_loc.split("#")[0]
                new_base = new_loc.split("#")[0]
                
                if old_base != new_base:
                    log.debug("Not the same page")
                    self.stack_pos += _direction
                    self.emit("history-pos-changed", self.stack_pos)
                return False
            
            old_loc = self.moz_widget.get_location()
            gobject.timeout_add(100, location_change, old_loc, direction)
            move_func()
        
    def go_back(self):
        """
        Go back one page, don't move in history if just anchor component of url was changed
        """
        self.__go(-1, self.moz_widget.can_go_back, self.moz_widget.go_back)
            
    def go_forward(self):
        """
        Go forward one page, don't move in history if just anchor component of url was changed
        """
        self.__go(1, self.moz_widget.can_go_forward, self.moz_widget.go_forward)
            
    def get_current_page_title(self):
        """
        Return title of current page
        """
        if self.stack_pos >= 0:
            return self.stack[self.stack_pos]['page_title']
    
    def get_current_page_url(self):
        """
        Return URL of current page
        """
        if self.stack_pos >= 0:
            return self.stack[self.stack_pos]['url']
        
    def get_current_page_books(self):
        """
        Return HTML code of current page
        """
        if self.stack_pos >= 0:
            return self.stack[self.stack_pos]['books_on_page']
        

class Browser(gobject.GObject):
    """
    Represents browser object
    """
    __gsignals__ = {
        'title-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str, ))
        }
    
    def __init__(self, statusbar, new_tab_callback, top_window, url=""):
        """
        Constructor. 
        
        @param statusbar: gtk.Statusbar object used for displaying messages
        @param new_tab_callback: callback function used for opening new tab/window
        @param top_window: gtk.Window object used for displaying various dialogs
        @param url: page url to be loaded by default
        """
        
        gobject.GObject.__init__(self)
        
        self.statusbar = statusbar
        self.current_url = None
        self.mouse_pressed = None
        self.new_tab_func = new_tab_callback
        self.top_window = top_window
        
        self.xml = gtk.glade.XML(path.get_data_path("pybookshelf.glade"), 
                                 root="browser_widget", domain="pybookshelf")
        self.widget = self.xml.get_widget("browser_widget")
        # workaround? because "expand" value from pybookshelf.glade is ignored 
        self.xml.get_widget("toolbutton3").set_expand(True)
        
        self.widget.set_data("browser", self)

        self.hist_back_button = self.get_widget("hist_back_button")
        self.hist_forward_button = self.get_widget("hist_forward_button")
        self.search_combo = self.get_widget("search_combo")
        self.search_combo_entry = self.get_widget("search_combo_entry")
        
        self.moz = gtkmozembed.MozEmbed()
        self.widget.add(self.moz)
        
        # initialize handlers
        
        self.history = BrowserHistory(self.moz, self.hist_back_button, self.hist_forward_button)
        self.history.connect("history-pos-changed", self.on_history_pos_changed)
        
        self.schema_pattern_re = re.compile("^([a-z]+)://(.*)")
        
        html.update_javascript_file()

        self.connect_events()
        #self.moz.realize()
        self.moz.show_all()
        
        if not "" == url:
            self.do_search(url)
        
    def __del__(self):
        """
        Browser "destructor", for debug puproses: to make sure that object is deleted when tab is closed
        """
        log.debug("Browser.__del__() called")
        
    def destroy(self):
        """
        Destroy Browser and all descendand objects: history navigation object, browser output window etc
        """
        log.debug("Explicitly destroy Browser object")
        # remove cache
        try:
            shutil.rmtree(self.history.cache_dir, ignore_errors=True)
        except IOError, e:
            log.error("Error occured during Browser object cache removing: %s" % e)
        
        # destroy manually created objects
        self.widget.set_data("browser", None)
        self.widget.destroy()
    
    def connect_events(self):
        """
        Connect handlers to required signals/events
        """
        self.moz.connect("open-uri", self.on_link_clicked)
        self.moz.connect("link-message", self.on_link_message)
        self.moz.connect("dom-mouse-click", self.on_dom_mouse_click)
        self.moz.connect("title", self.on_moz_title)
        self.hist_back_button.connect("clicked", self.on_hist_back)
        self.hist_forward_button.connect("clicked", self.on_hist_forward)
        self.search_combo_entry.connect("activate", self.on_search_combo_activate)
        self.get_widget("start_search_button").connect("clicked", self.on_search_combo_activate)
        self.get_widget("add_bookmark_button").connect("clicked", self.on_add_bookmark_button_clicked)
        
    def get_widget(self, widget_name):
        return self.xml.get_widget(widget_name)
      
    def focus_search(self):
        """
        Move keyboard focus to search combobox
        """
        self.search_combo.grab_focus()
        
    # display page functions
        
    def display_test_page(self, url):
        log.debug("Display test page")
        self.history.go_to("stat://test", content="<b>test</b>", title="Test page")
        
    def bookmark_page(self, url, title):
        """
        Add specified page to bookmarks
        """
        # TODO: add code for creating and displaying "add bookmark" dialog
        # for now just add
        bookmarks.add(target=url, title=title)
        
    def moz_book_rclick(self, args):
        """
        Display book popup menu (i.e. context menu for the link to the book)
        """    
    # MozEmbed popup menu items handlers
    def on_moz_title(self, widget):
        log.debug("Title changed")
        self.emit("title-changed", widget.get_title())
        
    def on_history_pos_changed(self, obj, new_pos):
        self.search_combo_entry.set_text(self.history.get_current_page_url())
       
    def do_search(self, pattern, update_history=True):
        """
        Perform search and display result in the browser
        """
        if collection.ScanThread.running:
            log.debug("While scan is in progress no other activity allowed")
            return
        
        mo = self.schema_pattern_re.match(pattern)
        if mo is None:
            # do a regular search
            self.__do_regular_search(pattern, update_history)
        else:
            self.on_link_clicked(self.moz, pattern)
            
    def display_html_file(self, html_file, page_files, page_title):
        body = ""
        self.history.go_to("file://%s" % html_file, path=html_file, page_files=page_files,
            title=page_title)
        
    # signal handlers
    def on_link_clicked(self, widget, url):
        """
        Process clicks on links in the browser
        
        @return: return True to disallow url processing by MozEmbed widget, 
            return False to allow internal URL opening by MozEmbed widget
        """
        log.debug("link clicked: %s" % url)
        if url.startswith("file://"):
            return False
        
        if url.startswith("newtab://"):
            self.new_tab_func(url[9:])
            return True
        
        # split url into `schema', `value' and `arguments' parts
        mo = self.schema_pattern_re.match(url)
        if mo is None:
            log.error("invalid url: %s" % url)
            return
        
        schema = mo.group(1)
        url_text = mo.group(2)
        
        arguments = dict()
        qpos = url_text.find("?")
        if qpos != -1 and qpos > 0:
            value = html.unquote(url_text[0:qpos])
            # find arguments
            for pair in url_text[qpos+1:].split("&"):
                try:
                    arg_k, arg_v = pair.split("=")
                    arguments[html.unquote(arg_k)] = html.unquote(arg_v)
                except ValueError:
                    continue
        else:
            value = html.unquote(url_text)
            
        if schema == "rclick":
            if 'type' not in arguments:
                return True
            
            rclick_type = arguments['type']
            if rclick_type in context_handlers:
                context_handlers[rclick_type](arguments, self.display_html_file,
                    self.bookmark_page)
            return True

        if "new_tab" in arguments:
            del arguments["new_tab"]
            self.new_tab_func(url)
            return True

        # always update history when clicking links
        if schema in url_handlers:
            try:
                result = url_handlers[schema](value, self.history.cache_dir, arguments)
                
                if isinstance(result, unicode) or isinstance(result, str):
                    # redirect detected
                    return self.on_link_clicked(widget, result)
                
                body, generated_files, page_title = result
                
                if isinstance(body, unicode):
                    self.history.go_to("%s://%s" % (schema, value), content=body, page_files=generated_files,
                        title=page_title)
                elif isinstance(body, tuple):
                    __type, __data = body
                    if __type=="file":
                        self.history.go_to("%s://%s" % (schema, value), path=__data, page_files=generated_files,
                            title=page_title)
                    
            except BaseException, e:
                core.error(_("Unable to open url %s://%s : %s" % (schema, value, e.message) ))
            
        return True
    
    def on_hist_back(self, widget):
        """
        Go to previous page in history
        """
        self.history.go_back()
    
    def on_hist_forward(self, widget):
        """
        Go to next page in history
        """
        self.history.go_forward()
       
    def on_search_combo_activate(self, widget, event=None):
        """
        Process event "activate", i.e. when user pressed <Enter> in the search entry box.
        """
        log.debug("search combo activated")
        # pattern is unicode encoded str!
        pattern = self.search_combo_entry.get_text()
        self.do_search(pattern, update_history=True)
        
    def on_add_bookmark_button_clicked(self, widget):
        """
        Process "click" event for "Bookmark current page" button.
        """
        # get URL of current page
        
        url = self.history.get_current_page_url()
        if url is None or url == "":
            return
        
        title = self.history.get_current_page_title()
        self.bookmark_page(url, title=title)
        core.information(_("Page “%s” has been saved to bookmarks as “%s”.") % (url, title))
        
    def on_link_message(self, widget):
        data = self.moz.get_link_message()
        text = ""
        
        if "" == data:
            self.statusbar.push(1, "")
            self.current_url = None
            return
        else:
            self.current_url = data
        
        if data.startswith("export://"):
            file_id = data[len("export://"):]
            book_file = collection.get_book_file(file_id)
            text = su.str2unicode(book_file)
            # get book file size
            try:
                book_file_size = os.path.getsize(book_file)
                text += " " + _("(%i bytes)") % book_file_size
            except OSError:
                pass
        elif data.startswith("author://"):
            text = _("Go to author page")
        elif data.startswith("book://"):
            text = _("Go to book page") + " " + data
        elif data.startswith("series://"):
            text = _("Go to series page")
        elif data.startswith("genre://"):
            text = _("Go to genre page")
        elif data.startswith("voteforbook://"):
            text = _("Set book rating")
        elif data.startswith("readbook://"):
            text = _("Read book")
    
        self.statusbar.push(1, text)

    def on_dom_mouse_click(self, widget, dom_event):
        if not self.current_url is None:
            # detect which mouse button was pressed
            pass 
        
    def __do_regular_search(self, pattern, update_history):
        if collection.ScanThread.running:
            log.debug("While scan is in progress no other activity allowed")
            return

        # TODO: move this code into module "collection"
        db = database.get_connection()
        cur = db.cursor()
        pattern = su.ustr2unicode(pattern)
        
        # file_id, author, title
        query = '''SELECT m1.file_id, f.plugin, m1.attr_value, m2.attr_value 
            FROM files f 
            JOIN metadata m1 ON f.file_id=m1.file_id
            JOIN metadata m2 ON m1.file_id=m2.file_id AND m1.attr_name="author" AND m2.attr_name="title"'''
            
        args = tuple()
        if pattern != "":
            parsed_query = parse_query_string(pattern)
            args = list()
            
            keywords_map = {'a': "author", 't': "title",
                            u'а': "author", u'т': "title"}
            intersect_queries = list()
            intersect_args = list()
            union_queries = list()
            union_args = list()
            subquery_template = '''
            SELECT m1.file_id, f.plugin, m1.attr_value, m2.attr_value 
                FROM files f 
                JOIN metadata m1 ON f.file_id=m1.file_id
                JOIN metadata m2 ON m1.file_id=m2.file_id AND m1.attr_name="author" AND m2.attr_name="title"'''
                
            subqueries = {
                'author': '''SELECT m1.file_id, f.plugin, m1.attr_value, m2.attr_value 
                    FROM files f
                    JOIN metadata m1 ON f.file_id=m1.file_id
                    JOIN metadata m2 ON m1.file_id=m2.file_id AND m1.attr_name="author" 
                    AND m2.attr_name="title" AND isubstr(?, m1.attr_value)''',
                
                'title': '''SELECT m1.file_id, f.plugin, m1.attr_value, m2.attr_value
                    FROM files f
                    JOIN metadata m1 ON f.file_id=m1.file_id
                    JOIN metadata m2 ON m1.file_id=m2.file_id AND m1.attr_name="author" 
                    AND m2.attr_name="title" AND isubstr(?, m2.attr_value)''',
                
                    }

            invalid_query = False
            for keyword, pat, sign in parsed_query:
                if sign != "" and sign != "+":
                    invalid_query = True
                    break

                # ignore unknown keywords
                if keyword not in keywords_map:
                    log.debug("query: %s" % parsed_query)
                    invalid_query = True
                    break
                    
                subquery = subqueries[keywords_map[keyword]]
                if sign == "+":
                    union_args.append(pat)
                    union_queries.append(subquery)
                else:
                    intersect_args.append(pat)
                    intersect_queries.append(subquery)
            
            if len(union_queries) > 0 and len(intersect_queries) == 0:
                invalid_query = True
                
            if invalid_query:
                log.debug("Invalid query.")
                query = '''
                SELECT m1.file_id, f.plugin, m1.attr_value, m2.attr_value 
                    FROM files f
                    JOIN metadata m1 ON f.file_id=m1.file_id
                    JOIN metadata m2 ON m1.file_id=m2.file_id AND m1.attr_name="author" AND m2.attr_name="title"
                        AND (isubstr(?, m1.attr_value) OR isubstr(?, m2.attr_value))'''
                args = (pattern, pattern)
            else:
                query = " INTERSECT ".join(intersect_queries)
                if len(union_queries) > 0:
                    query += " UNION "
                    query += " UNIOIN ".join(union_queries)
                    
                args = intersect_args + union_args
                
        
        timing = Timing(disabled=True)
        timing.start("Execute search query")
        database.execute(query, args=args, cursor=cur)
        timing.stop()

        # now prepare html page with results
        page_template = html.get_html_template("general_search_results")
        author_template = html.get_html_template("general_search_results_author")
        book_template = html.get_html_template("general_search_results_book")
        
        authors_dict = dict()
        books_dict = dict()
        
        display_rating = settings.get_boolean("ui/display_rating_on_search_results")
        
        i = 0
        timing.start("read results from cursor")
        for file_id, file_plugin, author_name, title in cur:
            i += 1
            if not authors_dict.has_key(author_name):
                # create branch in the dict with author's name
                authors_dict[author_name] = list()
            
            # now append file_id and title into dict entry that corresponds to given author
            authors_dict[author_name].append( (file_id, title, file_plugin) )
            
        total_books = i
        timing.stop()
        log.debug("Total results: %d" % i)
        
        # sort results
        timing.start("Sort results")
        authors_keys_sorted = sorted(authors_dict.keys())
        timing.stop()
        
        total_authors = len(authors_keys_sorted)
        log.debug("Total authors: %d" % total_authors)
        authors = ""
        
        log.debug("Generating results")
        i = 0
        html_star_on = '<img src="%s">' % html.stock_img_url("star.png")
        html_star_off = '<img src="%s">' % html.stock_img_url("star-bw.png")
        
        for author_name in authors_keys_sorted:
            log.debug("Author %d of %d " % (i, total_authors))
            i += 1

            # TODO: sort books_list
            books = ""
            for file_id, title, file_plugin in authors_dict[author_name]:
                rating = ""
                if display_rating:
                    rating = collection.get_book_rating(file_id)
                    stars = ""
                    if rating > 0:
                        # draw rating stars
                        for i in xrange(1, rating+1):
                            stars += html_star_on
                        for i in xrange(rating+1, 6):
                            stars += html_star_off
                            
                        rating = stars
                    else:
                        rating = ""
                    
                books += book_template % {
                    'file_id': file_id,
                    'book_title': title,
                    'rating': rating,
                    # TODO: detect file type more precisely
                    'file_type': file_plugin
                    }
            
            authors += author_template % {
                #'author_id': author_id,
                # without su.unicode2ustr() call this code is MUCH slower
                'author_name': su.unicode2ustr(author_name),
                'author_name_enc': html.quote(author_name),
                'books': su.unicode2ustr(books)
                }
        body = page_template % {
            'label_results': _("Search results for"),
            'search_pattern': escape(pattern),
            'results_num': gettext.ngettext("%d book found", "%d books found", total_books) % total_books,
            'results': authors                    
            }
        
        self.history.go_to(pattern, content=body,
            title=_(u"Search query: “%s”") % pattern)
        return True
